/*
 * Copyright 2007 Matt Jensen
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
 * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
 * specific language governing permissions and limitations under the License.
 */
package org.jtell.internal.config.annotation;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jtell.JTellException;
import org.jtell.config.Configuration;
import org.jtell.config.EventSinkMetadata;
import org.jtell.config.OrderMetadata;
import org.jtell.config.OrderType;
import org.jtell.config.annotation.Contribute;
import org.jtell.config.annotation.Event;
import org.jtell.config.annotation.Listener;
import org.jtell.config.annotation.Token;
import org.jtell.internal.Empties;
import org.jtell.internal.ReflectionUtils;
import org.jtell.internal.config.DefaultConfiguration;
import org.jtell.internal.config.EventSinkMetadataImpl;
import org.jtell.internal.config.OrderMetadataImpl;

import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;

/**
 * <p>
 * <code>AnnotationConfiguration</code> is an implementation of the {@link Configuration} interface which reads
 * configuration information from Java annotations on application listener classes. Since we cannot find and examine all
 * application listener classes at initialization time, we must examine each class when its first instance is
 * registered. That delayed introspection is handled by this class. 
 * </p>
 * <p>
 * <strong>Thread Safety</strong><br/>
 * Instances of this class are safe for multithreaded access.
 * </p>
 */
public class AnnotationConfiguration extends DefaultConfiguration
{
    private static Log LOG = LogFactory.getLog(AnnotationConfiguration.class);

    /**
     * <p>
     * Set holding the fully qualified names of all classes which have already been inspected.
     * </p>
     */

    private final Set<String> m_inspectedClassNames = new CopyOnWriteArraySet<String>();

    /**
     * <p>
     * Construct a {@link AnnotationConfiguration} instance.
     * </p>
     */
    public AnnotationConfiguration()
    {
        super();
    }

    /* inherit javadoc */
    @Override
    protected Set<EventSinkMetadata> getEventSinkMetadataForClass(final Class<?> clazz)
    {
        // We will have to inspect each class the first time we encounter it.
        final String className = clazz.getName();
        if (!m_inspectedClassNames.contains(className))
        {
            if (LOG.isDebugEnabled())
            {
                LOG.debug(String.format("Inspecting annotations for application listener class [%s].", className));
            }
            try
            {
                inspectClasses(ReflectionUtils.getAllSuperclassNames(className));
            }
            catch (ClassNotFoundException e)
            {
                throw new JTellException(String.format("Failed to load class [%s].", className), e);
            }
        }

        // The rest will be handled by our superclass.
        return super.getEventSinkMetadataForClass(clazz);
    }

    /**
     * <p>
     * Examine zero or more classes for event sink annotations. For each event sink method found on each class, register
     * an {@link EventSinkMetadata} instance with our superclass.
     * </p>
     * 
     * @param classNames the fully qualified names of the classes to examine.
     * @throws ClassNotFoundException if a class is not found.
     */
    private void inspectClasses(final List<String> classNames) throws ClassNotFoundException
    {
        for (final String className : classNames)
        {
            // Add the class to our set of inspected classes /before/ doing the inspection so that we don't re-inspect
            // in the event of a misconfiguration or class load error.
            m_inspectedClassNames.add(className);

            // Inspect the class.
            inspectClass(Class.forName(className));
        }
    }

    /**
     * <p>
     * Examine a class for event sink annotations. For each event sink method found on the class, register an
     * {@link EventSinkMetadata} instance with our superclass.
     * </p>
     *
     * @param clazz the class to examine.
     */
    private void inspectClass(final Class<?> clazz)
    {
        // Iterate over methods declared by the class.
        final String className = clazz.getName();
        for (final Method method : clazz.getDeclaredMethods())
        {
            // We are only interested in methods which are annotated with @Listener.
            final Listener listenerAnnotation = method.getAnnotation(Listener.class);
            if (null != listenerAnnotation)
            {
                addEventSinkMetadata(className, method, listenerAnnotation);
            }
        }
    }

    /**
     * <p>
     * Register an {@link EventSinkMetadata} instance with our superclass which describe the events to be passed to a
     * given event sink method. 
     * </p>
     *
     * @param className the fully qualified class name.
     * @param method the method.
     * @param listenerAnnotation the listener annotation.
     */
    private void addEventSinkMetadata(final String className, final Method method, final Listener listenerAnnotation)
    {
        // Get the declaring class and method signature.
        final String methodSignature = ReflectionUtils.getMethodSignature(method);

        // Get order metadata sets.
        final Set<OrderMetadata> afterOrders = createOrderMetadatas(OrderType.AFTER, listenerAnnotation.after());
        final Set<OrderMetadata> beforeOrders = createOrderMetadatas(OrderType.BEFORE, listenerAnnotation.before());

        // Get contribution tokens.
        final Contribute[] contributeAnnotations = listenerAnnotation.contribute();
        final Set<String> contributions;
        if (0 == contributeAnnotations.length)
        {
            contributions = Empties.EMPTY_CONTRIBUTIONS;
        }
        else
        {
            contributions = new HashSet<String>(contributeAnnotations.length);
            for (final Contribute contributeAnnotation : contributeAnnotations)
            {
                contributions.add(contributeAnnotation.value());
            }
        }

        // Iterate over event affinity entries.
        final Event[] eventAnnotations = listenerAnnotation.value();
        int eventSinkCount = 0;
        if (0 == eventAnnotations.length)
        {
            // No Event annotations, so register one event sink using the default event and source classes.
            addEventSinkMetadata(new EventSinkMetadataImpl(className, methodSignature,
                    getDefaultEventClass(method).getName(), getDefaultSourceClass(method).getName(),
                    Empties.EMPTY_ATTRIBUTES, afterOrders, beforeOrders, contributions));
            eventSinkCount = 1;
        }
        else
        {
            // Add (at most) one event sink per Event annotation. 
            for (final Event eventAnnotation : eventAnnotations)
            {
                // Build the attribute map.
                final Map<String, String> attributes = AnnotationUtils.getAttributes(eventAnnotation);

                // Get the event and source classes.
                final Class<?> annotatedEventClass = eventAnnotation.eventClass();
                final Class<?> eventClass;
                if (Void.class.equals(annotatedEventClass))
                {
                    eventClass = getDefaultEventClass(method);
                }
                else
                {
                    eventClass = annotatedEventClass;
                }
                final Class<?> annotatedSourceClass = eventAnnotation.sourceClass();
                final Class<?> sourceClass;
                if (Void.class.equals(annotatedSourceClass))
                {
                    sourceClass = getDefaultSourceClass(method);
                }
                else
                {
                    sourceClass = annotatedSourceClass;
                }

                // Create and add the event sink metadata instance.
                addEventSinkMetadata(new EventSinkMetadataImpl(className, methodSignature, eventClass.getName(),
                        sourceClass.getName(), attributes, afterOrders, beforeOrders, contributions));
                ++eventSinkCount; 
            }
        }
        if (LOG.isDebugEnabled())
        {
            LOG.debug(String.format("Added %d event sink(s) for method [%s] on listener class [%s].", eventSinkCount,
                    method, className));
        }
    }

    /**
     * <p>
     * Convert zero or more {@link Token} annotations into a set of order metadata objects.
     * </p>
     *
     * @param orderType the order type.
     * @param tokenAnnotations the token annotations.
     * @return {@link Set} of {@link OrderMetadata} instances.
     */
    private Set<OrderMetadata> createOrderMetadatas(final OrderType orderType, final Token[] tokenAnnotations)
    {
        final Set<OrderMetadata> result;
        if (0 == tokenAnnotations.length)
        {
            result = Empties.EMPTY_ORDER_METADATAS;
        }
        else
        {
            result = new HashSet<OrderMetadata>(tokenAnnotations.length);
            for (final Token tokenAnnotation : tokenAnnotations)
            {
                result.add(new OrderMetadataImpl(tokenAnnotation.value(), orderType, tokenAnnotation.required()));
            }
        }
        return result;
    }

    /**
     * <p>
     * Determine the default event class from a method signature.
     * </p>
     *
     * @param method the method.
     * @return {@link Class} instance.
     */
    private Class<?> getDefaultEventClass(final Method method)
    {
        // Attempt to determine the event class from the method signature.
        final Class<?> result;
        final Type[] parameterTypes = method.getGenericParameterTypes();
        if (0 == parameterTypes.length)
        {
            // Default to Object if the method takes no parameters.
            result = Object.class;
        }
        else if (parameterTypes.length > 1)
        {
            // Fail if the method takes more than one parameter.
            throw new IllegalStateException(String.format(
                    "Unable to determine event class for method [%s] because it takes too many parameters.",
                    ReflectionUtils.getMethodSignature(method)));
        }
        else
        {
            // Got exactly one parameter; infer from its type.
            if (parameterTypes[0] instanceof Class)
            {
                result = (Class)parameterTypes[0];
            }
            else if (parameterTypes[0] instanceof ParameterizedType)
            {
                result = (Class)((ParameterizedType)parameterTypes[0]).getRawType();
            }
            else
            {
                throw new IllegalStateException(String.format("Unable to determine event class for method [%s] because its parameter type is not recognized.", method));
            }
        }
        if (LOG.isDebugEnabled())
        {
            LOG.debug(String.format("Returning event class [%s] for method [%s].", result,
                    ReflectionUtils.getMethodSignature(method)));
        }
        return result;
    }

    /**
     * <p>
     * Determine the default source class from a method signature.
     * </p>
     *
     * @param method the method.
     * @return {@link Class} instance.
     */
    private Class<?> getDefaultSourceClass(final Method method)
    {
        // Attempt to determine the source class from the method signature.
        final Class<?> result;
        final Type[] parameterTypes = method.getGenericParameterTypes();
        if (0 == parameterTypes.length)
        {
            // Default to Object if the method takes no parameters.
            result = Object.class;
        }
        else if (parameterTypes.length > 1)
        {
            // Fail if the method takes more than one parameter.
            throw new IllegalStateException(String.format(
                    "Unable to determine source class for method [%s] because it takes too many parameters.",
                    ReflectionUtils.getMethodSignature(method)));
        }
        else
        {
            // Got exactly one parameter; attempt to infer from its type.
            if (parameterTypes[0] instanceof ParameterizedType)
            {
                // Method parameter is a parameterized type (generic.) Attempt to extract the source class.
                final ParameterizedType parameterizedType = (ParameterizedType)parameterTypes[0];
                final Type rawType = parameterizedType.getRawType();
                final Type[] typeArguments = parameterizedType.getActualTypeArguments();
                if (org.jtell.Event.class.equals(rawType))
                {
                    // For Event<?>, the event class is the type argument.
                    result = (Class)parameterizedType.getActualTypeArguments()[0];
                }
                else if (1 == typeArguments.length && typeArguments[0] instanceof Class)
                {
                    // Just assume that any single parameterized type is parameterized on the source class.
                    // TODO: May want to reel this back.
                    if (LOG.isInfoEnabled())
                    {
                        LOG.info(String.format(
                                "Assuming type argument [%s] on type [%s] as source class for method [%s].",
                                typeArguments[0], parameterizedType, method));
                    }
                    result = (Class)typeArguments[0];
                }
                else
                {
                    // Otherwise give up and assume Object.
                    result = Object.class;
                }
            }
            else if (parameterTypes[0] instanceof Class)
            {
                // Method parameter is a concrete class.
                final Class<?> clazz = (Class<?>)parameterTypes[0];
                if (org.jtell.Event.class.isAssignableFrom(clazz))
                {
                    // For anything assignable to Event<?>, get the type argument.
                    result = ReflectionUtils.getGenericInterfaceTypeParameter(clazz, org.jtell.Event.class, 0);
                }
                else
                {
                    // Otherwise give up and assume Object.
                    result = Object.class;
                }
            }
            else
            {
                result = Object.class;
            }
        }
        if (LOG.isDebugEnabled())
        {
            LOG.debug(String.format("Returning event class [%s] for method [%s].", result,
                    ReflectionUtils.getMethodSignature(method)));
        }
        return result;
    }

    /**
     * <p>
     * Stock empty event annotation array.
     * </p>
     */
    private final Event[] EMPTY_EVENTS = new Event[0];
}
